<?php
class DependencyInjectionError extends Exception { }

function instance($class) {
	static $instances = array();
	static $instantiating = array();
	$class = (string) $class;
	if (isset($instances[$class])) return $instances[$class];
	if (isset($instantiating[$class])) {
		$trace = debug_backtrace();
		for ($i = 1, $j = 3, $n = count($trace) - 2; $i < $n; ++$i, ++$j) {
			if ($trace[$i]['function'] == '__construct'
			 && isset($trace[$i]['object'])
			 && $trace[$i]['object'] instanceof $class
			 && $trace[$j]['function'] == 'instance'
			 && !isset($trace[$j]['class'])) {
				return $trace[$i]['object'];
			}
		}
		throw new DependencyInjectionError("Couldn't obtain an instance of $class: unsolvable recursiveness was detected");
	}
	$instantiating[$class] = true;
	if (!class_exists($class) && !interface_exists($class)) {
		throw new DependencyInjectionError("Couldn't obtain an instance of $class: no such class was found");
	}
	$refClass = new ReflectionClass($class);
	if ($refClass->hasMethod('getInstance')) {
		$getInstance = $refClass->getMethod('getInstance');
		if ($getInstance->isStatic() && $getInstance->isPublic()) {
			return $instances[$class] = $getInstance->invoke(null);
		}
	}
	if ($refClass->isInstantiable()) {
		if ($constructor = $refClass->getConstructor()) {
			$args = array();
			foreach ($constructor->getParameters() as $param) {
				if (!$paramClass = $param->getClass()) {
					throw new DependencyInjectionError("Couldn't obtain an instance of $class: a parameter of the constructor isn't type-hinted");
				}
				$args[] = instance($paramClass->name);
			}
			return $instances[$class] = $refClass->newInstanceArgs($args);
		} else {
			return $instances[$class] = $refClass->newInstance();
		}
	} else if ($refClass->isInterface()) {
		$implementors = array();
		foreach (get_declared_classes() as $candidate) {
			$refCandidate = new ReflectionClass($candidate);
			if ($refCandidate->implementsInterface($class) && $refCandidate->isInstantiable()) {
				$implementors[] = $candidate;
			}
		}
		switch (count($implementors)) {
			case 1:
				return $instances[$class] = instance($implementors[0]);
			case 0:
				throw new DependencyInjectionError("Couldn't obtain an instance of $class: there is no class implementing this interface");
			default:
				trigger_error("More than one implementor was found for interface $class, $implementors[0] was chosen from: ".implode(', ', $implementors), E_USER_WARNING);
				return $instances[$class] = instance($implementors[0]);
		}
	} else if ($refClass->isAbstract()) {
		$implementors = array();
		foreach (get_declared_classes() as $candidate) {
			$refCandidate = new ReflectionClass($candidate);
			if ($refCandidate->isSubclassOf($class) && $refCandidate->isInstantiable()) {
				$implementors[] = $candidate;
			}
		}
		switch (count($implementors)) {
			case 1:
				return $instances[$class] = instance($implementors[0]);
			case 0:
				throw new DependencyInjectionError("Couldn't obtain an instance of $class: there is no class implementing this abstract class");
			default:
				trigger_error("More than one implementor was found for abstract class $class, $implementors[0] was chosen from: ".implode(', ', $implementors), E_USER_WARNING);
				return $instances[$class] = instance($implementors[0]);
		}
	} else {
		throw new DependencyInjectionError("Couldn't obtain an instance of $class: class is non instantiable");
	}
}
